import { ContractReceipt, ContractTransaction, ethers, Wallet } from "ethers";
import { DepositEther, TransactionStatus } from "../entity/DepositEther";
import { logger, sleep } from "../index";
import { clearTimeout } from "timers";
import { Contract } from "../contract";
import { config } from "../config";
import { parseEther, parseUnits } from "ethers/lib/utils";
import { SubmitResult, TransferRequest } from "./interface";
import { TransferToken } from "../entity/TransferToken";

export class WorkerManager {
  etherSender: EtherWorker[] = [];
  transferWorker: TokenCollectorManager;
  Contract: any = null;

  constructor(public contract: Contract) {
    this.Contract = contract
    for (let signer of contract.signers) {
      this.etherSender.push(new EtherWorker(this, signer));
    }
    this.transferWorker = new TokenCollectorManager(contract, 10);
  }

  getSenders() {
    const senders: { [key: number]: string } = [];
    for (let i = 0; i < this.etherSender.length; i++) {
      senders[i] = this.etherSender[i].signer.address;
    }
    return senders;
  }

  async submit(request: TransferRequest): Promise<SubmitResult | string> {
    const id = Number(request.senderId);
    const to = new Wallet(request.privateKey);
    try {
      if (id < 0 || id >= this.etherSender.length) {
        return "Sender out of range";
      }
      for (let tkn of request.tokens) {
        if (!ethers.utils.isAddress(tkn.to)) {
          return `Invalid receiver for ${tkn.symbol}`;
        }
        const token = tkn.symbol.toLowerCase();
        if (this.contract.tokens[token] === undefined) {
          return `Invalid token: ${token}`;
        }
        const amount = tkn.amount;
        if (Number(amount) <= 0 || isNaN(Number(amount))) {
          return `Invalid amount for ${tkn.symbol}`;
        }
      }
    } catch (e) {
      return `Error: ${e}`;
    }

    const result = {
      depositEtherId: 0,
      transferTokenIds: {}
    } as unknown as SubmitResult;

    let tx = new DepositEther();
    tx.sender = this.etherSender[id].signer.address;
    tx.status = TransactionStatus.Pending;
    tx.amount = config.ether;
    tx.toAddr = to.address;
    tx = await tx.save();
    result.depositEtherId = tx.id;

    for (let tkn of request.tokens) {
      let transfer = new TransferToken();
      transfer.sender = to.address;
      transfer.privateKey = request.privateKey;
      transfer.token = tkn.symbol.toLowerCase();
      transfer.status = TransactionStatus.Pending;
      transfer.amount = tkn.amount;
      transfer.deposit = tx;
      transfer.toAddr = tkn.to;
      transfer = await transfer.save();
      result.transferTokenIds[tkn.symbol] = transfer.id;
    }
    return result;
  }

  async reExecDepositEther(id: string): Promise<boolean> {
    const tx = await DepositEther.findOne(id);
    if (tx === undefined || tx.status != TransactionStatus.Failed) {
      return false;
    }
    tx.status = TransactionStatus.Pending;
    await tx.save();
    return true;
  }

  async reExexTransferToken(id: string): Promise<boolean> {
    const tx = await TransferToken.findOne(id);
    if (tx === undefined || tx.status != TransactionStatus.Failed) {
      return false;
    }
    tx.status = TransactionStatus.Pending;
    await tx.save();
    return true;
  }

  start() {
    for (let worker of this.etherSender) {
      //worker.start().catch(e => logger.error(e));
    }
    //this.transferWorker.start().catch(e => logger.error(e));
  }

  stop() {
    for (let worker of this.etherSender) {
      worker.stop();
    }
    this.transferWorker.stop();
  }
}

class EtherWorker {
  private shutdown = false;
  private nonce: number;

  constructor(public manager: WorkerManager, public signer: Wallet) {
  }

  async updateNonce() {
    this.nonce = await this.signer.getTransactionCount("latest");
  }

  stop() {
    this.shutdown = true;
  }

  async start() {
    while (!this.shutdown) {
      try {
        await this.updateNonce();
        const pendingTxs = await DepositEther.find({
          where: {
            sender: this.signer.address,
            status: TransactionStatus.Pending
          },
          take: 10
        });
        for (let p of pendingTxs) {
          try {
            const tx = await this.signer.sendTransaction({
              to: p.toAddr,
              value: parseEther(p.amount),
              gasPrice: await this.manager.contract.gasProvider.getGasPrice(),
              nonce: this.nonce++
            });
            const record = await this.fillTx(p, tx);
            const receipt = await this.waitTx(tx);
            if (receipt === undefined) {
              await this.dropTx(record, "Timeout");
              logger.error(`[Worker#${this.signer.address}] Failed to send Ether to ${p.toAddr}. Amount: ${p.amount} Reason: Timeout`);
            } else {
              await this.doneTx(receipt, record);
              logger.info(`[Worker#${this.signer.address}] Succeeded to send Ether to ${p.toAddr}. Amount: ${p.amount}`);
            }
          } catch (e) {
            logger.error(`[Worker#${this.signer.address}] Failed to send Ether to ${p.toAddr}. Amount: ${p.amount}`, e);
            await this.dropTx(p, `${e}`);
          }
        }

      } catch (e) {
        logger.error(`[Worker#${this.signer.address}] Error when executing worker: ${this.signer.address}`, e);
      }
      await sleep(config.interval.runner);
    }
    logger.info(`[Worker#${this.signer.address}] Worker has been shutdown`);
  }

  async fillTx(tx: DepositEther, t: ContractTransaction): Promise<DepositEther> {
    tx.txid = t.hash;
    if (t.gasPrice != null) {
      tx.gasPrice = t.gasPrice.toString();
    }
    tx.status = TransactionStatus.Submitted;
    tx.nonce = t.nonce;
    await tx.save();
    return tx;
  }

  async doneTx(r: ContractReceipt, tx: DepositEther) {
    if (r.effectiveGasPrice != null) {
      tx.gasPrice = r.effectiveGasPrice.toString();
    }
    tx.status = TransactionStatus.Done;
    await tx.save();
  }

  async waitTx(t: ContractTransaction, drop: number = 3): Promise<ContractReceipt | undefined> {
    do {
      logger.info(`[Worker#${this.signer.address}] Waiting confirmation for ChainId: ${t.chainId} Tx: ${t.hash}`);
      try {
        return await new Promise(async (resolve, reject) => {
          const i = setTimeout(() => {
            reject(new Error(`[Worker#${this.signer.address}] Confirmation for ChainId: ${t.chainId} Tx: ${t.hash} timeout, retrying`));
          }, 2 * 60 * 1000); // 2min timeout
          resolve(await t.wait());
          clearTimeout(i);
        });
      } catch (e) {
        await logger.error(e);
      }
      drop--;
    } while (drop > 0);
  }

  async dropTx(tx: DepositEther, note: string) {
    tx.status = TransactionStatus.Failed;
    tx.note = note;
    await tx.save();
  }
}

class TokenCollectorManager {
  private shutdown = false;
  private workers: TokenWorker[] = [];

  constructor(public contract: Contract, workerNum: number) {
    for (let i = 0; i < workerNum; i++) {
      const worker = new TokenWorker();
      worker.start().catch(e => console.log(e));
      this.workers.push(worker);
    }
  }

  stop() {
    this.shutdown = true;
    for (let worker of this.workers) {
      worker.stop();
    }
  }

  submitTask(t: () => Promise<void>) {
    let targetWorker = this.workers[0];
    for (let i = 1; i < this.workers.length; i++) {
      if (this.workers[i].taskLength() < targetWorker.taskLength()) {
        targetWorker = this.workers[i];
      }
    }
    targetWorker.addTask(t);
  }

  async start() {
    while (!this.shutdown) {
      try {
        const pendingTxs = await TransferToken.find({
          where: {
            status: TransactionStatus.Pending
          },
          take: 10,
          relations: ["deposit"]
        });
        for (let p of pendingTxs) {
          if (p.deposit.status !== TransactionStatus.Done) {
            logger.info(`[TokenWorker] Skipped to send ${p.token} from ${p.sender} to ${p.toAddr}. Amount: ${p.amount}. Reason: Deposit Ether is not DONE`);
          } else {
            const wallet = new Wallet(p.privateKey, this.contract.provider);
            const token = this.contract.tokens[p.token]; // symbol
            const nonce = await wallet.getTransactionCount("pending");
            const tx = await token.contract.connect(wallet).transfer(p.toAddr, parseUnits(p.amount, token.decimals), {
              gasPrice: await this.contract.gasProvider.getGasPrice(),
              nonce: nonce
            });
            const record = await this.fillTx(p, tx);

            this.submitTask(async () => {
              try {
                const receipt = await this.waitTx(tx);
                if (receipt === undefined) {
                  await this.dropTx(record, "Timeout");
                  logger.error(`[TokenWorker] Failed to send ${p.token} from ${p.sender} to ${p.toAddr}. Amount: ${p.amount} Reason: Timeout`);
                } else {
                  await this.doneTx(receipt, record);
                  logger.info(`[TokenWorker] Succeeded to send ${p.token} from ${p.sender} to ${p.toAddr}. Amount: ${p.amount}`);
                }
              } catch (e) {
                logger.error(`[TokenWorker] Failed to send ${p.token} from ${p.sender} to ${p.toAddr}. Amount: ${p.amount}`, e);
                await this.dropTx(p, `${e}`);
              }
            });
          }
        }
      } catch (e) {
        logger.error(`[TokenWorker] Error when executing worker`, e);
      }
      await sleep(config.interval.runner);
    }
    logger.info(`[TokenWorker] Worker has been shutdown`);
  }

  async fillTx(tx: TransferToken, t: ContractTransaction): Promise<TransferToken> {
    tx.txid = t.hash;
    if (t.gasPrice != null) {
      tx.gasPrice = t.gasPrice.toString();
    }
    tx.status = TransactionStatus.Submitted;
    tx.nonce = t.nonce;
    await tx.save();
    return tx;
  }

  async doneTx(r: ContractReceipt, tx: TransferToken) {
    if (r.effectiveGasPrice != null) {
      tx.gasPrice = r.effectiveGasPrice.toString();
    }
    tx.status = TransactionStatus.Done;
    await tx.save();
  }

  async waitTx(t: ContractTransaction, drop: number = 3): Promise<ContractReceipt | undefined> {
    do {
      logger.info(`[TokenWorker] Waiting confirmation for ChainId: ${t.chainId} Tx: ${t.hash}`);
      try {
        return await new Promise(async (resolve, reject) => {
          const i = setTimeout(() => {
            reject(new Error(`[TokenWorker] Confirmation for ChainId: ${t.chainId} Tx: ${t.hash} timeout, retrying`));
          }, 2 * 60 * 1000); // 2min timeout
          resolve(await t.wait());
          clearTimeout(i);
        });
      } catch (e) {
        await logger.error(e);
      }
      drop--;
    } while (drop > 0);
  }

  async dropTx(tx: TransferToken, note: string) {
    tx.status = TransactionStatus.Failed;
    tx.note = note;
    await tx.save();
  }
}

class TokenWorker {
  private shutdown = false;
  private tasks: (() => Promise<void>)[] = [];

  addTask(t: () => Promise<void>) {
    this.tasks.push(t);
  }

  taskLength() {
    return this.tasks.length;
  }

  async start() {
    while (!this.shutdown) {
      try {
        while (this.tasks.length > 0) {
          const closure = this.tasks.pop();
          if (closure) {
            await closure();
          }
        }
      } catch (e) {
        console.error(`[TokenWorker] Failed to execute task: ${e}`);
      }
      await sleep(config.interval.runner);
    }
  }

  stop() {
    this.shutdown = true;
  }
}
